/*!
 * Ext JS Library 3.2.1
 * Copyright(c) 2006-2010 Ext JS, Inc.
 * licensing@extjs.com
 * http://www.extjs.com/license
 */
Ext.ns('Ext.ux.grid');

/**
 * @class Ext.ux.grid.RowEditor
 * @extends Ext.Panel
 * Plugin (ptype = 'roweditor') that adds the ability to rapidly edit full rows in a grid.
 * A validation mode may be enabled which uses AnchorTips to notify the user of all
 * validation errors at once.
 *
 * @ptype roweditor
 */
Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, {
   floating: true,
   shadow: false,
   layout: 'hbox',
   cls: 'x-small-editor',
   buttonAlign: 'center',
   baseCls: 'x-row-editor',
   elements: 'header,footer,body',
   frameWidth: 5,
   buttonPad: 3,
   clicksToEdit: 'auto',
   monitorValid: true,
   focusDelay: 250,
   errorSummary: true,
   saveText: 'Save',
   cancelText: 'Cancel',
   commitChangesText: 'You need to commit or cancel your changes',
   errorText: 'Errors',
   defaults: {
      normalWidth: true
   },

   initComponent: function () {
      Ext.ux.grid.RowEditor.superclass.initComponent.call(this);
      this.addEvents(
      /**
       * @event beforeedit
       * Fired before the row editor is activated.
       * If the listener returns <tt>false</tt> the editor will not be activated.
       * @param {Ext.ux.grid.RowEditor} roweditor This object
       * @param {Number} rowIndex The rowIndex of the row just edited
       */
      'beforeedit',
      /**
       * @event canceledit
       * Fired when the editor is cancelled.
       * @param {Ext.ux.grid.RowEditor} roweditor This object
       * @param {Boolean} forced True if the cancel button is pressed, false is the editor was invalid.
       */
      'canceledit',
      /**
       * @event validateedit
       * Fired after a row is edited and passes validation.
       * If the listener returns <tt>false</tt> changes to the record will not be set.
       * @param {Ext.ux.grid.RowEditor} roweditor This object
       * @param {Object} changes Object with changes made to the record.
       * @param {Ext.data.Record} r The Record that was edited.
       * @param {Number} rowIndex The rowIndex of the row just edited
       */
      'validateedit',
      /**
       * @event afteredit
       * Fired after a row is edited and passes validation.  This event is fired
       * after the store's update event is fired with this edit.
       * @param {Ext.ux.grid.RowEditor} roweditor This object
       * @param {Object} changes Object with changes made to the record.
       * @param {Ext.data.Record} r The Record that was edited.
       * @param {Number} rowIndex The rowIndex of the row just edited
       */
      'afteredit');
   },

   init: function (grid) {
      this.grid = grid;
      this.ownerCt = grid;
      if (this.clicksToEdit === 2) {
         grid.on('rowdblclick', this.onRowDblClick, this);
      } else {
         grid.on('rowclick', this.onRowClick, this);
         if (Ext.isIE) {
            grid.on('rowdblclick', this.onRowDblClick, this);
         }
      }

      // stopEditing without saving when a record is removed from Store.
      grid.getStore().on('remove', function () {
         this.stopEditing(false);
      }, this);

      grid.on({
         scope: this,
         keydown: this.onGridKey,
         columnresize: this.verifyLayout,
         columnmove: this.refreshFields,
         reconfigure: this.refreshFields,
         beforedestroy: this.beforedestroy,
         destroy: this.destroy,
         bodyscroll: {
            buffer: 250,
            fn: this.positionButtons
         }
      });
      grid.getColumnModel().on('hiddenchange', this.verifyLayout, this, {
         delay: 1
      });
      grid.getView().on('refresh', this.stopEditing.createDelegate(this, []));
   },

   beforedestroy: function () {
      this.stopMonitoring();
      this.grid.getStore().un('remove', this.onStoreRemove, this);
      this.stopEditing(false);
      Ext.destroy(this.btns, this.tooltip);
   },

   refreshFields: function () {
      this.initFields();
      this.verifyLayout();
   },

   isDirty: function () {
      var dirty;
      this.items.each(function (f) {
         if (String(this.values[f.id]) !== String(f.getValue())) {
            dirty = true;
            return false;
         }
      }, this);
      return dirty;
   },

   startEditing: function (rowIndex, doFocus) {
      if (this.editing && this.isDirty()) {
         this.showTooltip(this.commitChangesText);
         return;
      }
      if (Ext.isObject(rowIndex)) {
         rowIndex = this.grid.getStore().indexOf(rowIndex);
      }
      if (this.fireEvent('beforeedit', this, rowIndex) !== false) {
         this.editing = true;
         var g = this.grid,
            view = g.getView(),
            row = view.getRow(rowIndex),
            record = g.store.getAt(rowIndex);

         this.record = record;
         this.rowIndex = rowIndex;
         this.values = {};
         if (!this.rendered) {
            this.render(view.getEditorParent());
         }
         var w = Ext.fly(row).getWidth();
         this.setSize(w);
         if (!this.initialized) {
            this.initFields();
         }
         var cm = g.getColumnModel(),
            fields = this.items.items,
            f, val;
         for (var i = 0, len = cm.getColumnCount(); i < len; i++) {
            val = this.preEditValue(record, cm.getDataIndex(i));
            f = fields[i];
            f.setValue(val);
            this.values[f.id] = Ext.isEmpty(val) ? '' : val;
         }
         this.verifyLayout(true);
         if (!this.isVisible()) {
            this.setPagePosition(Ext.fly(row).getXY());
         } else {
            this.el.setXY(Ext.fly(row).getXY(), {
               duration: 0.15
            });
         }
         if (!this.isVisible()) {
            this.show().doLayout();
         }
         if (doFocus !== false) {
            this.doFocus.defer(this.focusDelay, this);
         }
      }
   },

   stopEditing: function (saveChanges) {
      this.editing = false;
      if (!this.isVisible()) {
         return;
      }
      if (saveChanges === false || !this.isValid()) {
         this.hide();
         this.fireEvent('canceledit', this, saveChanges === false);
         return;
      }
      var changes = {},
         r = this.record,
         hasChange = false,
         cm = this.grid.colModel,
         fields = this.items.items;
      for (var i = 0, len = cm.getColumnCount(); i < len; i++) {
         if (!cm.isHidden(i)) {
            var dindex = cm.getDataIndex(i);
            if (!Ext.isEmpty(dindex)) {
               var oldValue = r.data[dindex],
                  value = this.postEditValue(fields[i].getValue(), oldValue, r, dindex);
               if (String(oldValue) !== String(value)) {
                  changes[dindex] = value;
                  hasChange = true;
               }
            }
         }
      }
      if (hasChange && this.fireEvent('validateedit', this, changes, r, this.rowIndex) !== false) {
         r.beginEdit();
         Ext.iterate(changes, function (name, value) {
            r.set(name, value);
         });
         r.endEdit();
         this.fireEvent('afteredit', this, changes, r, this.rowIndex);
      }
      this.hide();
   },

   verifyLayout: function (force) {
      if (this.el && (this.isVisible() || force === true)) {
         var row = this.grid.getView().getRow(this.rowIndex);
         this.setSize(Ext.fly(row).getWidth(), Ext.isIE ? Ext.fly(row).getHeight() + 9 : undefined);
         var cm = this.grid.colModel,
            fields = this.items.items;
         for (var i = 0, len = cm.getColumnCount(); i < len; i++) {
            if (!cm.isHidden(i)) {
               var adjust = 0;
               if (i === (len - 1)) {
                  adjust += 3; // outer padding
               } else {
                  adjust += 1;
               }
               fields[i].show();
               fields[i].setWidth(cm.getColumnWidth(i) - adjust);
            } else {
               fields[i].hide();
            }
         }
         this.doLayout();
         this.positionButtons();
      }
   },

   slideHide: function () {
      this.hide();
   },

   initFields: function () {
      var cm = this.grid.getColumnModel(),
         pm = Ext.layout.ContainerLayout.prototype.parseMargins;
      this.removeAll(false);
      for (var i = 0, len = cm.getColumnCount(); i < len; i++) {
         var c = cm.getColumnAt(i),
            ed = c.getEditor();
         if (!ed) {
            ed = c.displayEditor || new Ext.form.DisplayField();
         }
         if (i == 0) {
            ed.margins = pm('0 1 2 1');
         } else if (i == len - 1) {
            ed.margins = pm('0 0 2 1');
         } else {
            if (Ext.isIE) {
               ed.margins = pm('0 0 2 0');
            }
            else {
               ed.margins = pm('0 1 2 0');
            }
         }
         ed.setWidth(cm.getColumnWidth(i));
         ed.column = c;
         if (ed.ownerCt !== this) {
            ed.on('focus', this.ensureVisible, this);
            ed.on('specialkey', this.onKey, this);
         }
         this.insert(i, ed);
      }
      this.initialized = true;
   },

   onKey: function (f, e) {
      if (e.getKey() === e.ENTER) {
         this.stopEditing(true);
         e.stopPropagation();
      }
   },

   onGridKey: function (e) {
      if (e.getKey() === e.ENTER && !this.isVisible()) {
         var r = this.grid.getSelectionModel().getSelected();
         if (r) {
            var index = this.grid.store.indexOf(r);
            this.startEditing(index);
            e.stopPropagation();
         }
      }
   },

   ensureVisible: function (editor) {
      if (this.isVisible()) {
         this.grid.getView().ensureVisible(this.rowIndex, this.grid.colModel.getIndexById(editor.column.id), true);
      }
   },

   onRowClick: function (g, rowIndex, e) {
      if (this.clicksToEdit == 'auto') {
         var li = this.lastClickIndex;
         this.lastClickIndex = rowIndex;
         if (li != rowIndex && !this.isVisible()) {
            return;
         }
      }
      this.startEditing(rowIndex, false);
      this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
   },

   onRowDblClick: function (g, rowIndex, e) {
      this.startEditing(rowIndex, false);
      this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
   },

   onRender: function () {
      Ext.ux.grid.RowEditor.superclass.onRender.apply(this, arguments);
      this.el.swallowEvent(['keydown', 'keyup', 'keypress']);
      this.btns = new Ext.Panel({
         baseCls: 'x-plain',
         cls: 'x-btns',
         elements: 'body',
         layout: 'table',
         width: (this.minButtonWidth * 2) + (this.frameWidth * 2) + (this.buttonPad * 4),
         // width must be specified for IE
         items: [{
            ref: 'saveBtn',
            itemId: 'saveBtn',
            xtype: 'button',
            text: this.saveText,
            width: this.minButtonWidth,
            handler: this.stopEditing.createDelegate(this, [true])
         },
         {
            xtype: 'button',
            text: this.cancelText,
            width: this.minButtonWidth,
            handler: this.stopEditing.createDelegate(this, [false])
         }]
      });
      this.btns.render(this.bwrap);
   },

   afterRender: function () {
      Ext.ux.grid.RowEditor.superclass.afterRender.apply(this, arguments);
      this.positionButtons();
      if (this.monitorValid) {
         this.startMonitoring();
      }
   },

   onShow: function () {
      if (this.monitorValid) {
         this.startMonitoring();
      }
      Ext.ux.grid.RowEditor.superclass.onShow.apply(this, arguments);
   },

   onHide: function () {
      Ext.ux.grid.RowEditor.superclass.onHide.apply(this, arguments);
      this.stopMonitoring();
      this.grid.getView().focusRow(this.rowIndex);
   },

   positionButtons: function () {
      if (this.btns) {
         var g = this.grid,
            h = this.el.dom.clientHeight,
            view = g.getView(),
            scroll = view.scroller.dom.scrollLeft,
            bw = this.btns.getWidth(),
            width = Math.min(g.getWidth(), g.getColumnModel().getTotalWidth());

         this.btns.el.shift({
            left: (width / 2) - (bw / 2) + scroll,
            top: h - 2,
            stopFx: true,
            duration: 0.2
         });
      }
   },

   // private
   preEditValue: function (r, field) {
      var value = r.data[field];
      return this.autoEncode && typeof value === 'string' ? Ext.util.Format.htmlDecode(value) : value;
   },

   // private
   postEditValue: function (value, originalValue, r, field) {
      return this.autoEncode && typeof value == 'string' ? Ext.util.Format.htmlEncode(value) : value;
   },

   doFocus: function (pt) {
      if (this.isVisible()) {
         var index = 0,
            cm = this.grid.getColumnModel(),
            c;
         if (pt) {
            index = this.getTargetColumnIndex(pt);
         }
         for (var i = index || 0, len = cm.getColumnCount(); i < len; i++) {
            c = cm.getColumnAt(i);
            if (!c.hidden && c.getEditor()) {
               c.getEditor().focus();
               break;
            }
         }
      }
   },

   getTargetColumnIndex: function (pt) {
      var grid = this.grid,
         v = grid.view,
         x = pt.left,
         cms = grid.colModel.config,
         i = 0,
         match = false;
      for (var len = cms.length, c; c = cms[i]; i++) {
         if (!c.hidden) {
            if (Ext.fly(v.getHeaderCell(i)).getRegion().right >= x) {
               match = i;
               break;
            }
         }
      }
      return match;
   },

   startMonitoring: function () {
      if (!this.bound && this.monitorValid) {
         this.bound = true;
         Ext.TaskMgr.start({
            run: this.bindHandler,
            interval: this.monitorPoll || 200,
            scope: this
         });
      }
   },

   stopMonitoring: function () {
      this.bound = false;
      if (this.tooltip) {
         this.tooltip.hide();
      }
   },

   isValid: function () {
      var valid = true;
      this.items.each(function (f) {
         if (!f.isValid(true)) {
            valid = false;
            return false;
         }
      });
      return valid;
   },

   // private
   bindHandler: function () {
      if (!this.bound) {
         return false; // stops binding
      }
      var valid = this.isValid();
      if (!valid && this.errorSummary) {
         this.showTooltip(this.getErrorText().join(''));
      }
      this.btns.saveBtn.setDisabled(!valid);
      this.fireEvent('validation', this, valid);
   },

   lastVisibleColumn: function () {
      var i = this.items.getCount() - 1,
         c;
      for (; i >= 0; i--) {
         c = this.items.items[i];
         if (!c.hidden) {
            return c;
         }
      }
   },

   showTooltip: function (msg) {
      var t = this.tooltip;
      if (!t) {
         t = this.tooltip = new Ext.ToolTip({
            maxWidth: 600,
            cls: 'errorTip',
            width: 300,
            title: this.errorText,
            autoHide: false,
            anchor: 'left',
            anchorToTarget: true,
            mouseOffset: [40, 0]
         });
      }
      var v = this.grid.getView(),
         top = parseInt(this.el.dom.style.top, 10),
         scroll = v.scroller.dom.scrollTop,
         h = this.el.getHeight();

      if (top + h >= scroll) {
         t.initTarget(this.lastVisibleColumn().getEl());
         if (!t.rendered) {
            t.show();
            t.hide();
         }
         t.body.update(msg);
         t.doAutoWidth(20);
         t.show();
      } else if (t.rendered) {
         t.hide();
      }
   },

   getErrorText: function () {
      var data = ['<ul>'];
      this.items.each(function (f) {
         if (!f.isValid(true)) {
            data.push('<li>', f.getActiveError(), '</li>');
         }
      });
      data.push('</ul>');
      return data;
   }
});
Ext.preg('roweditor', Ext.ux.grid.RowEditor);